#include "node.hpp"
#include "packet.hpp"
#include "cookie.hpp"
#include <gvl/system/system.hpp>
#include <gvl/support/bits.hpp>
#include <gvl/support/log.hpp>
#include <gvl/support/algorithm.hpp>

namespace Psync5
{

void Node::initSockets(int portNew)
{
	sock.close();
	port = portNew;
	sock = gvl::udp_socket();

	sock.bind(port);
}

void Node::host(int port)
{
	initSockets(port);

	acceptConnections(true);
}

Link* Node::connect(char const* address, int port)
{
	initSockets(0);

	gvl::internet_addr addr(address, port);

	Link* newLink = addLink_(addr);
	newLink->initiate();

	serverLink_ = newLink;

	acceptConnections(false);
	return newLink;
}

Link* Node::addLink_(gvl::internet_addr addr)
{
	Link* link = newLink(this, nextLinkId++, addr, sock);
	links.push_back(link);

	linkFromId_.insert(LinkFromIdMap::value_type(link->id, link));

	return link;
}

Link* Node::findLink_(gvl::internet_addr addr)
{
	auto r = gvl::drop_while(links.all(),
		[&addr] (Link& l) { return l.address() != addr; });

	if(!r.empty())
		return &r.front();
	return 0;
}

void Node::send(Packet const& packet, gvl::internet_addr const& addr)
{
	int ret = packet.trySend(sock, addr);

	if(ret <= 0)
	{
		LOG("Error sending packet: " << ret);
	}
}

void Node::process()
{
	while(true)
	{
		gvl::internet_addr sourceAddr;
		uint8_t buf[2048];

		int r = sock.recvfrom(buf, sizeof(buf), sourceAddr);
		if(r == gvl::socket::would_block || r == 0)
			break;
		if(r < 0)
		{
			TLOG(this << ": socket error!");
			//continue;
			break;
		}

		TLOG(this << ": received packet, size " << r);

		Packet packet(buf, buf + r, buf + r);

		// TODO: Catch any exceptions that might be thrown
		PacketHeader header = packet.readHeader();

		switch(header.type)
		{
			case PacketHeader::Init:
			{
				if(acceptConnections_)
				{
					TLOG(this << ": received Init");
					uint8_t buf[256];
					Packet initAck(buf, buf + 256);
					initAck.writeHeader(PacketHeader(PacketHeader::InitAck, 0));

					InitCookie cookie(0, 0);

					initAck.write64(cookie.x);
					initAck.write64(cookie.y);
					initAck.write32(cookie.timer);

					initAck.trySend(sock, sourceAddr);
					break;
				}
			}
			
			case PacketHeader::InitAck:
			{
				if(Link* link = findLink_(sourceAddr))
				{
					// TODO: Catch any exceptions that might be thrown
					TLOG(this << ": received InitAck");
					link->handleInitAck(packet);
				}
				
				break;
			}

			case PacketHeader::Cookie:
			{
				InitCookie recvCookie;

				// TODO: Catch any exceptions that might be thrown

				recvCookie.x = packet.read64();
				recvCookie.y = packet.read64();
				recvCookie.timer = packet.read32();

				bool ok = recvCookie.verify(0, 0);
				if(ok)
				{
					Link* link = findLink_(sourceAddr);
					if(!link)
						link = addLink_(sourceAddr);

					// TODO: Pass packet to newLink to process any data

					if(link->handleCookie())
					{
						onLinkConnected(*link);
					}
				}

				break;
			}

			case PacketHeader::CookieAck:
			{
				if(Link* link = findLink_(sourceAddr))
				{
					TLOG(this << ": received CookieAck");
					if(link->handleCookieAck(packet))
					{
						onLinkConnected(*link);
					}
				}
				break;
			}

			case PacketHeader::Normal:
			{
				if(Link* link = findLink_(sourceAddr))
				{
					// TODO: Catch any exceptions that might be thrown by recv
					link->recv(packet);
				}
				break;
			}

			case PacketHeader::Shutdown:
			{
				if(Link* link = findLink_(sourceAddr))
				{
					link->handleShutdown(packet);
				}
				break;
			}

			case PacketHeader::ShutdownAck:
			{
				if(Link* link = findLink_(sourceAddr))
				{
					link->handleShutdownAck(packet);
				}
				break;
			}

			case PacketHeader::ShutdownComplete:
			{
				if(Link* link = findLink_(sourceAddr))
				{
					link->handleShutdownComplete(packet);
				}
				break;
			}
		}
	}

	for(auto r = links.all(); !r.empty(); )
	{
		if(r.front().disconnected())
		{
			onLinkDisconnected(r.front());
			if(&r.front() == serverLink_)
				serverLink_ = 0;
			links.erase_front(r);
		}
		else
		{
			r.front().process();
			r.pop_front();
		}
	}
}

}
